Liquidity Profile Analysis¶

Import Libraries¶

In [1]:
import requests
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
import json
import math
import datetime
from tqdm import tqdm

Configuration¶

In [2]:
# # Load configuration
# with open("WETH_USDC_arbitrum_3000_config.json", 'r') as f:
#     config = json.load(f)

config = {
    "base_symbol": "0",
    "base_token": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
    "quote_token": "0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8",
    "decimal_0": "18",
    "decimal_1": "6"
}

# Determine token order
if config["base_symbol"] == "0":
    token_0, token_1 = config['base_token'], config['quote_token']
else:
    token_0, token_1 = config['quote_token'], config['base_token']

# Example Pool Addresses
NETWORK_MAINNET = 'eth_mainnet'
MAINNET_USDC_30BP = '0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8'
MAINNET_USDC_5BP = '0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640'

NETWORK_ARBITRUM = 'arbitrum'
ARB_USDC_5BP = '0xc6962004f452be9203591991d15f6b388e09e8d0'

# Time range
INI_BLOCK = 155687991
FIN_BLOCK = 176212562

Helper Functions¶

In [3]:
def transform_amount_0(amount0):
    return int(amount0) * 10 ** -int(config["decimal_0"])

def transform_amount_1(amount1):
    return int(amount1) * 10 ** -int(config["decimal_1"])

def tick_2_price(tick):
    decimal_0 = int(config["decimal_0"])
    decimal_1 = int(config["decimal_1"])
    base_symbol = config["base_symbol"]
    
    token_0_price = 1.0001 ** tick * 10 ** (decimal_0 - decimal_1)
    return token_0_price if base_symbol == "0" else 1 / token_0_price

def price_2_tick(price):
    decimal_0 = int(config["decimal_0"])
    decimal_1 = int(config["decimal_1"])
    base_symbol = config["base_symbol"]
    
    token_0_price = price if base_symbol == "0" else 1 / price
    if token_0_price == 0:
        return -math.inf
    
    tick = math.log(token_0_price, 1.0001) + (decimal_1 - decimal_0) * math.log(10, 1.0001)
    return round(tick)

def get_datetime_from_blocknumber(block_number):
    arbitrum_node_url = "https://arb-mainnet.g.alchemy.com/v2/nHzD3Ofjd2yRam6T9HYBjgtIqp2l8i2K"
    payload = {
        "jsonrpc": "2.0",
        "method": "eth_getBlockByNumber",
        "params": [hex(block_number), False],
        "id": 1
    }
    
    try:
        response = requests.post(arbitrum_node_url, json=payload)
        response.raise_for_status()
        timestamp = int(response.json()["result"]["timestamp"], 16)
        return datetime.datetime.utcfromtimestamp(timestamp)
    except requests.exceptions.RequestException as e:
        print(f"An error occurred: {e}")
        return None

API Parameters¶

The API endpoint for fetching liquidity data is:

  • URL: http://office-ml.teahouse.finance:8181/pools/liquidity
  • Method: GET
  • Parameters:
    • pool: Smart contract address of the liquidity pool
    • block (optional): Specific block number or "latest" (default: "latest")
    • network (optional): Blockchain network (default: "eth_mainnet")

Time and Price Range¶

In [4]:
# Time range
print(f"Start date: {get_datetime_from_blocknumber(INI_BLOCK)}")
print(f"End date: {get_datetime_from_blocknumber(FIN_BLOCK)}")

# Price range calculation
price = 200  # Example price
tick = price_2_tick(price)
tick_lower = price_2_tick(0.8 * price)
tick_upper = price_2_tick(1.25 * price)
print(f"Tick range: {tick - tick_lower} (lower), {tick_upper - tick} (upper)")
Start date: 2023-12-01 00:00:00
End date: 2024-02-01 00:00:00
Tick range: 2232 (lower), 2231 (upper)

Main Functions¶

In [5]:
def get_LP_df(block, network=NETWORK_ARBITRUM, pool=ARB_USDC_5BP, range_lower=2230, range_upper=2230):
    path = f'http://office-ml.teahouse.finance:8181/pools/liquidity?network={network}&block={block}&pool={pool}'
    
    try:
        raw = requests.get(path).json()
        if raw is None:
            raise TypeError(f'API error. No data received. Current block: {block}')
        
        while not raw['success']:
            raw = requests.get(path).json()
    except requests.exceptions.RequestException as e:
        print(f'API connection error: {e}. Current block: {block}')
        return None
    except TypeError as e:
        print(e)
        return None
    
    data = raw['data']
    current_tick = np.floor(data['tick'] / 10) * 10
    tick_lower = current_tick - range_lower
    tick_upper = current_tick + range_upper
    
    tick_resample = pd.DataFrame({'ticks': np.arange(np.min(data['ticks']), np.max(data['ticks']) + 10, 10)})
    df = pd.DataFrame(data).merge(tick_resample, how='right').ffill()
    df = df[df['ticks'].between(tick_lower, tick_upper, 'both')].copy()
    
    df_rotate = df[['ticks', 'liquidity']].T
    df_rotate.columns = np.arange(-range_lower, range_upper + 10, 10)
    df_rotate = df_rotate.drop('ticks').reset_index().drop(columns='index')
    df_rotate.insert(0, 'price', df['price0'].values[0])
    df_rotate.insert(1, 'tick', df['tick'].values[0])
    df_rotate.index = [block]
    
    return df_rotate

def LP_profile(ini_block=INI_BLOCK, fin_block=FIN_BLOCK, h = 1, network=NETWORK_ARBITRUM, pool=ARB_USDC_5BP):
    block_diff = int(h*14400)
    df_list = []
    #for block in np.arange(ini_block, fin_block + 1, 14400):
    for block in tqdm(np.arange(ini_block, fin_block + 1, block_diff)):
        df = get_LP_df(block, network, pool)
        if df is not None:
            df_list.append(df)
    
    df_LP = pd.concat(df_list)
    df_LP.index.name = 'blockNumber'
    return df_LP

Usage Examples¶

In [6]:
# Example 1: Get liquidity profile for a specific block
lp_data = get_LP_df(250216394, network=NETWORK_ARBITRUM, pool=ARB_USDC_5BP)
print(lp_data.head())
                 price      tick              -2230              -2220  \
250216394  2407.857488 -198456.0  65319128404743974  65317708380180194   

                       -2210              -2200              -2190  \
250216394  65317707420994702  65317706479255557  65922475316990807   

                       -2180              -2170              -2160  ...  \
250216394  65918421117372642  65918420210575981  65476739682030715  ...   

                          2140                 2150                 2160  \
250216394  2298778648364058955  2181321853172914261  2181565779301741685   

                          2170                 2180                 2190  \
250216394  2172484536418228451  2234380006049459731  2166225040768634533   

                          2200                 2210                 2220  \
250216394  2105930947724359136  2585744329154382117  2678250644703325821   

                          2230  
250216394  2576314153690487211  

[1 rows x 449 columns]
In [7]:
# Time range
INI_BLOCK = 155687991
FIN_BLOCK = 176212562

# h=5 means use every 5th block.  ideally should be binned instead
h = 5

# Example 2: Get liquidity profile over a range of blocks
lp_profile = LP_profile(INI_BLOCK, FIN_BLOCK, h = h)
print(lp_profile.head())
100%|████████████████████████████████████████████████████████████████████████████████| 286/286 [22:36<00:00,  4.74s/it]
                   price      tick                 -2230  \
blockNumber                                                
155687991    2051.612845 -200057.0  230197839079941088.0   
155759991    2094.303730 -199851.0  252267184765966432.0   
155831991    2092.044645 -199862.0  252193044622096576.0   
155903991    2084.387919 -199898.0  254062828852266784.0   
155975991    2092.133036 -199861.0  251100708057670400.0   

                            -2220                 -2210                 -2200  \
blockNumber                                                                     
155687991    230197839079941088.0  230197839079941088.0  230210442796650048.0   
155759991    252191400256621312.0  253467730047109824.0  253467730047109824.0   
155831991    252267184765966432.0  252191400256621312.0  253467730047109824.0   
155903991    254020379421091008.0  251452975186504192.0  252090772447810784.0   
155975991    251174848201540256.0  251099063692195136.0  252375393482683616.0   

                            -2190                 -2180                 -2170  \
blockNumber                                                                     
155687991    230418736520182624.0  253238506699890048.0  254247675409071712.0   
155759991    253467730047109824.0  252880119352108384.0  254549883221558080.0   
155831991    253467730047109824.0  253467730047109824.0  252880119352108384.0   
155903991    252164912591680672.0  252089128082335520.0  253365457872824032.0   
155975991    252375393482683616.0  252375393482683616.0  251787782787682208.0   

                            -2160  ...                  2140  \
blockNumber                        ...                         
155687991    256374839332466688.0  ...  256705466883063392.0   
155759991    302316489695047296.0  ...  244586593717825472.0   
155831991    254549883221558080.0  ...  242465426647041216.0   
155903991    253365457872824032.0  ...  242465426647041216.0   
155975991    253457546657131904.0  ...  242330657779851712.0   

                             2150                  2160                  2170  \
blockNumber                                                                     
155687991    256705466883063392.0  254784560262612448.0  254784560262612448.0   
155759991    244586593717825472.0  195433979485914944.0  194484135775501920.0   
155831991    242465426647041216.0  242465426647041216.0  193312812415130688.0   
155903991    242465426647041216.0  242465426647041216.0  242465426647041216.0   
155975991    242330657779851712.0  242330657779851712.0  193178043547941184.0   

                             2180                  2190                  2200  \
blockNumber                                                                     
155687991    244586593717825472.0  244586593717825472.0  244586593717825472.0   
155759991    194484135775501920.0  194326396850945344.0  194326396850945344.0   
155831991    192362968704717696.0  192362968704717696.0  192205229780161120.0   
155903991    242465426647041216.0  242465426647041216.0  193312812415130688.0   
155975991    192228199837528192.0  192228199837528192.0  192205229780161120.0   

                             2210                  2220                  2230  
blockNumber                                                                    
155687991    244586593717825472.0  244586593717825472.0  244586593717825472.0  
155759991    194326396850945344.0  194326396850945344.0  194326396850945344.0  
155831991    192205229780161120.0  192205229780161120.0  192205229780161120.0  
155903991    192362968704717696.0  192362968704717696.0  192205229780161120.0  
155975991    192205229780161120.0  192205229780161120.0  192205229780161120.0  

[5 rows x 449 columns]
In [8]:
# Example 3: Visualize liquidity distribution
plt.figure(figsize=(12, 6))
plt.plot(lp_data.columns[2:], lp_data.iloc[0, 2:])
plt.title('Liquidity Distribution')
plt.xlabel('Tick')
plt.ylabel('Liquidity')
plt.show()
No description has been provided for this image

(Tick, Time) -> Liquidity Surface¶

Visualization¶

In [9]:
lp_profile

# Constants
SECONDS_IN_YEAR = 365.25 * 24 * 3600  # 31,557,600 seconds
BLOCK_TIME_SECONDS = 1  # Arbitrum block time

# Determine the first block number
first_block = lp_profile.index.min()

# Calculate time in years from the first observation
lp_profile['time'] = (lp_profile.index - first_block) * BLOCK_TIME_SECONDS / SECONDS_IN_YEAR
In [21]:
# Reset index to have 'block' as a column
df_reset = lp_profile.reset_index().rename(columns={'index': 'blockNumber'})

# Melt the DataFrame to long format
df = df_reset.melt(id_vars=['blockNumber', 'price', 'tick', 'time'], 
                        var_name='tick_level', 
                        value_name='liquidity')
df['liquidity'] = pd.to_numeric(df['liquidity'])
In [24]:
# Transform the 'tick_level' from string to integer
df['tick_level'] = df['tick_level'].astype(int)

# Add the 'y' column
df['y'] = np.log(df['liquidity'])

print(df.head())
print(df.tail())
   blockNumber        price      tick      time  tick_level     liquidity  \
0    155687991  2051.612845 -200057.0  0.000000       -2230  2.301978e+17   
1    155759991  2094.303730 -199851.0  0.002282       -2230  2.522672e+17   
2    155831991  2092.044645 -199862.0  0.004563       -2230  2.521930e+17   
3    155903991  2084.387919 -199898.0  0.006845       -2230  2.540628e+17   
4    155975991  2092.133036 -199861.0  0.009126       -2230  2.511007e+17   

           y  
0  39.977716  
1  40.069265  
2  40.068971  
3  40.076358  
4  40.064630  
        blockNumber        price      tick      time  tick_level  \
127837    175919991  2337.341007 -198753.0  0.641113        2230   
127838    175991991  2335.387048 -198761.0  0.643395        2230   
127839    176063991  2300.651759 -198911.0  0.645676        2230   
127840    176135991  2342.705172 -198730.0  0.647958        2230   
127841    176207991  2285.705318 -198976.0  0.650240        2230   

           liquidity          y  
127837  3.928340e+17  40.512164  
127838  3.956828e+17  40.519389  
127839  4.407689e+17  40.627297  
127840  3.919023e+17  40.509789  
127841  4.534801e+17  40.655728  

3D Surface Plot using Matplotlib¶

In [25]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D  # Import the 3D plotting toolkit

# Assuming 'df' is your DataFrame with columns 'tick', 'time', and 'y'
# Create a pivot table to reshape the data for plotting
pivot_df = df.pivot(index='time', columns='tick_level', values='y')

# Generate meshgrid for plotting
X, Y = np.meshgrid(pivot_df.columns.values, pivot_df.index.values)
Z = pivot_df.values

# Create a 3D plot
fig = plt.figure(figsize=(12, 8))
ax = fig.add_subplot(111, projection='3d')

# Plot the surface
surf = ax.plot_surface(X, Y, Z, cmap='viridis', edgecolor='none')

# Add color bar and labels
fig.colorbar(surf, ax=ax, shrink=0.5, aspect=5)
ax.set_xlabel('Tick')
ax.set_ylabel('Time')
ax.set_zlabel('log(Liquidity)')
ax.set_title('3D Surface Plot of log(Liquidity)')

plt.show()
No description has been provided for this image

Contour Plot using Matplotlib¶

In [26]:
import matplotlib.pyplot as plt

# Create contour plot
plt.figure(figsize=(12, 8))
cp = plt.contourf(X, Y, Z, levels=50, cmap='viridis')
plt.colorbar(cp, label='log(Liquidity)')

plt.xlabel('Tick')
plt.ylabel('Time')
plt.title('Contour Plot of log(Liquidity)')

plt.show()
No description has been provided for this image

Heatmap using Seaborn¶

In [27]:
import seaborn as sns
import matplotlib.pyplot as plt

# Using the pivoted DataFrame from the previous example
plt.figure(figsize=(14, 10))
sns.heatmap(pivot_df, cmap='viridis', cbar_kws={'label': 'log(Liquidity)'})

plt.xlabel('Tick')
plt.ylabel('Time')
plt.title('Heatmap of log(Liquidity)')

plt.show()
No description has been provided for this image

Interactive 3D Plot using Plotly¶

In [28]:
import plotly.graph_objs as go
import plotly.offline as pyo

# Prepare data for Plotly
fig = go.Figure(data=[go.Surface(z=Z, x=pivot_df.columns.values, y=pivot_df.index.values)])

# Update layout for better visuals
fig.update_layout(
    title='Interactive 3D Surface Plot of log(Liquidity)',
    autosize=True,
    width=800,
    height=800,
    scene=dict(
        xaxis_title='Tick',
        yaxis_title='Time',
        zaxis_title='log(Liquidity)'
    )
)

# Display the plot in the notebook
pyo.iplot(fig)
In [29]:
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from IPython.display import HTML

# Get unique time values
unique_times = sorted(df['time'].unique())

# Set up the figure and axis
fig, ax = plt.subplots(figsize=(10, 6))

# Initialize the line object to be updated
line, = ax.plot([], [], lw=2)

# Define the initialization function
def init():
    ax.set_xlim(df['tick_level'].min(), df['tick_level'].max())
    ax.set_ylim(df['y'].min(), df['y'].max())
    ax.set_xlabel('Tick')
    ax.set_ylabel('log(Liquidity)')
    ax.set_title('Liquidity over Ticks')
    return line,

# Define the animation function
def animate(i):
    data = df[df['time'] == unique_times[i]]
    line.set_data(data['tick_level'], data['y'])
    ax.set_title(f'Liquidity over Ticks at Time {unique_times[i]}')
    return line,

# Create the animation
ani = FuncAnimation(fig, animate, frames=len(unique_times), init_func=init, blit=True)

# Display the animation in the notebook
HTML(ani.to_jshtml())
Out[29]:
No description has been provided for this image
No description has been provided for this image

GP Imports¶

In [30]:
# GP Imports
import torch
import gpytorch

# some settings I found that help with numerics
from settings import set_gpytorch_settings
set_gpytorch_settings()

# for custom kernels
from gpytorch.kernels import ScaleKernel, RBFKernel, LinearKernel, PeriodicKernel, MaternKernel
from kernels import PowerExponentialKernel, GeneralCauchy, MinKernel, FractionalGaussianNoise, fBMKernel
In [31]:
# Ensure the data is sorted by 'tick_level' and 'time'
df = df.sort_values(['tick_level', 'time'])
df['t'] = df['time'] / df['time'][1]
In [39]:
df['t']
Out[39]:
0           0.0
1           1.0
2           2.0
3           3.0
4           4.0
          ...  
127837    281.0
127838    282.0
127839    283.0
127840    284.0
127841    285.0
Name: t, Length: 127842, dtype: float64
In [40]:
max_lag = 50  # Adjust based on your data
In [41]:
# Get the unique tick levels
tick_levels = df['tick_level'].unique()

# Initialize the covariance matrix
# Rows: tick levels x, Columns: lags h
covariance_matrix = pd.DataFrame(index=tick_levels, columns=range(max_lag + 1), dtype=float)
In [42]:
for x in tick_levels:
    # Extract the time series f(x, t) for tick level x
    df_x = df[df['tick_level'] == x].sort_values('time')
    f_xt = df_x['y'].values

    # Mean of f(x, t)
    mean_f_xt = np.mean(f_xt)

    # Number of observations
    N = len(f_xt)

    # Compute autocovariance for lags h = 0 to max_lag
    for h in range(max_lag + 1):
        if h < N:
            # Shift the series by h
            f_xt_h = f_xt[h:]
            f_xt_trimmed = f_xt[:N - h]

            # Subtract the mean
            f_xt_trimmed_centered = f_xt_trimmed - mean_f_xt
            f_xt_h_centered = f_xt_h - mean_f_xt

            # Compute autocovariance
            cov = np.sum(f_xt_trimmed_centered * f_xt_h_centered) / (N - h)
            covariance_matrix.loc[x, h] = cov
        else:
            # Not enough data points for this lag
            covariance_matrix.loc[x, h] = np.nan
In [43]:
# Convert indices and columns to float for plotting
covariance_matrix.index = covariance_matrix.index.astype(float)
covariance_matrix.columns = covariance_matrix.columns.astype(float)

# Create meshgrid for plotting
X, Y = np.meshgrid(covariance_matrix.columns, covariance_matrix.index)
Z = covariance_matrix.values
In [45]:
# Create the 3D surface plot
fig = go.Figure(data=[go.Surface(z=Z, x=X, y=Y)])

# Update layout for better visuals
fig.update_layout(
    title='Autocovariance Function cov(f(x,t), f(x,t+h)) as a function of x and h',
    autosize=True,
    width=800,
    height=800,
    scene=dict(
        xaxis_title='Lag h',
        yaxis_title='Tick x',
        zaxis_title='Autocovariance'
    )
)

# Display the plot
pyo.iplot(fig)
In [ ]: